﻿/*
COPYRIGHT NOTICE

Copyright (c) 2011, S James S Stapleton
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
	  this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
	  this list of conditions and the following disclaimer in the documentation
	  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

namespace ConfigFileManager
{
    public static class ConfigFile
    {
        static private Encoding c_default_encoding = Encoding.Default /*Encoding.UTF8*/;
        /// <summary>
        /// Get/set the default encoding used for reading/writing files.
        /// Uses the system default initially. Note: switching encodings is not thread safe.
        /// </summary>
        static public Encoding default_encoding
        {
            get
            {
                return c_default_encoding;
            }
            set
            {
                c_default_encoding = value;
            }
        }
        /// <summary>
        /// Set the ConfigFile tool to use the system default encoding. Note: switching encodings is not thread safe.
        /// </summary>
        static public void SystemDefaultEncoding()
        {
            c_default_encoding = Encoding.Default;
        }
        /// <summary>
        /// Honor bom? to determine encoding. Set to false to always use the default/set encoding.
        /// </summary>
        static public bool honor_bom = true;

        static public readonly int c_version_major = 0;
        static public readonly int c_version_minor = 4;
        static public readonly int c_version_build = 9;
        static public readonly string c_version_string = "ConfigFileManager Version " + c_version_major.ToString() +
            "." + c_version_minor.ToString() + "." + c_version_build.ToString() + " (alpha)";

        static public int c_line_length = 80;
        /// <summary>
        /// Get/Set the length of lines in Write()'ed files
        /// </summary>
        static public int line_length
        {
            get
            {
                return c_line_length;
            }
            set
            {
                c_line_length = value;
            }
        }
        /// <summary>
        /// New line characters, for splitting strings
        /// </summary>
        static public readonly string[] c_newlines = new string[] { "\r\n", "\n", "\r" };
        /// <summary>
        /// Whitespace, used for acceptable line splits
        /// </summary>
        static public readonly char[] c_whitespace = new char[] { ' ', '\t', '\v', '(', ')', '[', ']', '{', '}' };

        /// <summary>
        /// Read the config file. Lines starting with a non-null '#' are ignored. Blank lines are ignored.
        /// </summary>
        /// <param name="fname">The name of the file the entries were read from.</param>
        /// <param name="outd">The configuration file is placed into outd.</param>
        /// <param name="lines">The file content, split by lines, to parse into outd.</param>
        static public void Read(string fname, ref Dictionary<string, string> outd, string[] lines)
        {
            string key;
            string val;
            bool append;
            int i = 0;
            while(GetNextKeyValue(fname, ref i, lines, out append, out key, out val))
            {
                if(append && outd.ContainsKey(key))
                    outd[key]+= val;
                else
                    outd[key] = val;
            }
        }
        /// <summary>
        /// Read the config file. Lines starting with a non-null '#' are ignored. Blank lines are ignored.
        /// </summary>
        /// <param name="fname">The name of the file the entries were read from. It will be opened and read</param>
        /// <param name="outd">The configuration file is placed into outd.</param>
        static public void Read(string fname, ref Dictionary<string, string> outd)
        {
            using(FileStream fp = new FileStream(fname, FileMode.Open, FileAccess.Read))
            {
                byte[] readbuff = new byte[fp.Length];
                int pos = 0;
                while(pos < fp.Length)
                    pos += fp.Read(readbuff, pos, (int)fp.Length-pos);
                Encoding enc = c_default_encoding;
                //try to honor BOM
                if(honor_bom)
                {
                    get_decoder(ref readbuff, ref enc);
                }
                string buff = enc.GetString(readbuff);
                string[] lines = buff.Split(c_newlines, StringSplitOptions.None);
                Read(fname, ref outd, lines);
            }
        }
        /// <summary>
        /// Read the config file. Lines starting with a non-null '#' are ignored. Blank lines are ignored.
        /// </summary>
        /// <param name="fname">The name of the file the entries were read from.</param>
        /// <param name="keyorder">The order of the keys, read from the file, </param>
        /// <param name="outd">The configuration file is placed into outd.</param>
        /// <param name="lines">The file content, split by lines, to parse into outd.</param>
        /// <param name="comments">Comments are stored here. Set passed arg to null to not read comments.</param> 
        static public void Read(string fname, out string[] keyorder, ref Dictionary<string, string> outd, ref Dictionary<string,string> comments, string[] lines)
        {
            List<string> keyo = new List<string>();
            string key;
            string val;
            bool append;
            string comment;
            int i = 0;
            while(GetNextKeyValue(fname, ref i, lines, out append, out key, out val, out comment))
            {
                if(append && outd.ContainsKey(key))
                    outd[key] += val;
                else
                    outd[key] = val;
                if(!keyo.Contains(key)) keyo.Add(key);

                if(comments != null)
                {
                    if(append && comments.ContainsKey(key))
                        comments[key] += comment;
                    else
                        comments[key] = comment;
                }
            }
            keyorder = keyo.ToArray<string>();
        }
        /// <summary>
        /// Read the config file. Lines starting with a non-null '#' are ignored. Blank lines are ignored.
        /// </summary>
        /// <param name="fname">The name of the file the entries were read from. It will be opened and read</param>
        /// <param name="keyorder">The order of the keys, read from the file, </param>
        /// <param name="outd">The configuration file is placed into outd.</param>
        /// <param name="comments">Comments are stored here. Set passed arg to null to not read comments.</param>
        static public void Read(string fname, out string[] keyorder, ref Dictionary<string, string> outd, ref Dictionary<string,string> comments)
        {
            List<string> keyo = new List<string>();
            using(FileStream fp = new FileStream(fname, FileMode.Open, FileAccess.Read))
            {
                byte[] readbuff = new byte[fp.Length];
                int pos = 0;
                while(pos < fp.Length)
                    pos += fp.Read(readbuff, pos, (int)fp.Length - pos);
                Encoding enc = c_default_encoding;
                //try to honor BOM
                if(honor_bom)
                {
                    get_decoder(ref readbuff, ref enc);
                }
                string buff = enc.GetString(readbuff);
                string[] lines = buff.Split(c_newlines, StringSplitOptions.None);
                Read(fname, out keyorder, ref outd, ref comments, lines);
            }
        }
        /// <summary>
        /// Read the config file. Lines starting with a non-null '#' are ignored. Blank lines are ignored.
        /// </summary>
        /// <param name="fname">The name of the file the entries were read from.</param>
        /// <param name="keyorder">The order of the keys, read from the file, </param>
        /// <param name="outd">The configuration file is placed into outd.</param>
        /// <param name="lines">The file content, split by lines, to parse into outd.</param>
        /// <param name="comments">Comments are stored here. There will be one entry per key. If left null, comments won't be retrieved.</param> 
        static public void Read(string fname, out IList<string> keyorder, ref Dictionary<string, IList<string>> outd, ref Dictionary<string, IList<string>> comments, string[] lines)
        {
            outd.Clear();
            if(comments != null) comments.Clear();
            keyorder = new List<string>();
            string key;
            string val;
            bool append;
            string comment;
            int i = 0;
            while(GetNextKeyValue(fname, ref i, lines, out append, out key, out val, out comment))
            {
                if(!append || !outd.ContainsKey(key))
                    outd[key] = new List<string>();
                outd[key].Add(val);

                if(comments != null)
                {
                    if(comment==null)comment="";
                    if(!append || !comments.ContainsKey(key))
                        comments[key] = new List<string>();
                    comments[key].Add(comment);
                }
                ((List<string>)keyorder).Add(key);
            }
        }
        /// <summary>
        /// Read the config file. Lines starting with a non-null '#' are ignored. Blank lines are ignored.
        /// </summary>
        /// <param name="fname">The name of the file the entries were read from. It will be opened and read</param>
        /// <param name="keyorder">The order of the keys, read from the file, </param>
        /// <param name="outd">The configuration file is placed into outd.</param>
        /// <param name="comments">Comments are stored here. Set passed arg to null to not read comments.</param>
        static public void Read(string fname, out IList<string> keyorder, ref Dictionary<string, IList<string>> outd, ref Dictionary<string, IList<string>> comments)
        {
            List<string> keyo = new List<string>();
            using(FileStream fp = new FileStream(fname, FileMode.Open, FileAccess.Read))
            {
                byte[] readbuff = new byte[fp.Length];
                int pos = 0;
                while(pos < fp.Length)
                    pos += fp.Read(readbuff, pos, (int)fp.Length - pos);
                Encoding enc = c_default_encoding;
                //try to honor BOM
                if(honor_bom)
                {
                    get_decoder(ref readbuff, ref enc);
                }
                string buff = enc.GetString(readbuff);
                string[] lines = buff.Split(c_newlines, StringSplitOptions.None);
                Read(fname, out keyorder, ref outd, ref comments, lines);
            }
        }
        /// <summary>
        /// Writes a configuration file. Any carrage return, newline, and carrage return + newlines found in keys or
        /// values will be replaced by newlines when they are escaped. I.E. The following C# string:
        /// "\n \r \r\n" will be written to file as this C# string "\\n \\n \\n", or if opened a text editor, this exact
        /// string: `\n \n \n`.
        /// </summary>
        /// <param name="fname">The name of the output file</param>
        /// <param name="pairs">The configuration to write</param>
        /// <param name="comments">Comments. Each comment is given a key, the comment is written before that value.</param>
        /// <param name="order">Write keys in this order. Keys not in this list will appear later, in arbitrary order</param>
        static public void Write(string fname, Dictionary<string, string> pairs, Dictionary<string, string> comments, string[] order)
        {
            //step one, generate the output to write.
            StringBuilder output = new StringBuilder();
            string[] entry;
            if(order != null && order.Length > 0)
            {
                for(int i = 0; i < order.Length; i++)
                {
                    entry = GetEntry(order[i], pairs, comments);
                    foreach(string s in entry)
                        output.Append(s);
                    output.Append(Environment.NewLine);
                }
            }
            foreach(string s in pairs.Keys)
            {
                if(order.Contains<string>(s)) continue;
                entry = GetEntry(s, pairs, comments);
                foreach(string st in entry)
                    output.Append(st);
                output.Append(Environment.NewLine);
            }

            byte[] outp = c_default_encoding.GetBytes(output.ToString());
            output = null;

            using(FileStream fp = new FileStream(fname, FileMode.Create, FileAccess.Write))
            {
                fp.Write(outp, 0, outp.Length);
            }
        }
        /// <summary>
        /// Writes a configuration file. Any carrage return, newline, and carrage return + newlines found in keys or
        /// values will be replaced by newlines when they are escaped. I.E. The following C# string:
        /// "\n \r \r\n" will be written to file as this C# string "\\n \\n \\n", or if opened a text editor, this exact
        /// string: `\n \n \n`.
        /// </summary>
        /// <param name="fname">The name of the output file</param>
        /// <param name="pairs">The configuration to write</param>
        /// <param name="comments">Comments. Each comment is given a key, the comment is written before that value.</param>
        /// <param name="order">Write keys in this order. Keys not in this list will appear later, in arbitrary order</param>
        static public void Write(string fname, Dictionary<string, IList<string>> pairs, Dictionary<string, IList<string>> comments, IList<string> order)
        {
            //step one, generate the output to write.
            StringBuilder output = new StringBuilder();
            Dictionary<string, int> saved_to = new Dictionary<string, int>(pairs.Count);
            foreach(string key in pairs.Keys)
            {
                saved_to[key] = 0;
            }
            string[] entry;
            if(order != null && order.Count > 0)
            {
                for(int i = 0; i < order.Count; i++)
                {
                    string key = order[i];
                    int p = saved_to[key];
                    entry = GetEntry(key, p, pairs, comments);
                    foreach(string s in entry)
                        output.Append(s);
                    output.Append(Environment.NewLine);
                    saved_to[key]++;
                }
            }
            foreach(string s in pairs.Keys)
            {
                int p = saved_to[s];
                while(p < pairs[s].Count)
                {
                    entry = GetEntry(s, p, pairs, comments);
                    foreach(string st in entry)
                        output.Append(st);
                    output.Append(Environment.NewLine);
                    p++;
                }
            }

            byte[] outp = c_default_encoding.GetBytes(output.ToString());
            output = null;

            using(FileStream fp = new FileStream(fname, FileMode.Create, FileAccess.Write))
            {
                fp.Write(outp, 0, outp.Length);
            }
        }
        /// <summary>
        /// Get the output config lines for the config file.
        /// Note that comments are not escaped, but values and keys are!
        /// </summary>
        /// <param name="key">The config file key for the entry of interest</param>
        /// <param name="pairs">The config key/value file values</param>
        /// <param name="comments">The comments for some keys of interest</param>
        /// <returns>The lines for the specified entry in a config file</returns>
        static private string[] GetEntry(string key, Dictionary<string, string> pairs, Dictionary<string, string> comments)
        {
            List<string> ret = new List<string>();
            string val = null;
            string[] vals;
            if(comments != null && comments.TryGetValue(key, out val) && val != null)
            {
                if(c_line_length > 10)
                {
                    vals = SplitLine(val, c_line_length - 2);
                    foreach(string v in vals)
                        if(v != null)
                            ret.Add(string.Format("# {0}{1}", v, Environment.NewLine));
                }
                else
                    ret.Add(string.Format("# {0}{1}", val, Environment.NewLine));
            }
            val=pairs[key];
            if(val==null)
                return new string[] { "" };
            val = Escape(key) + "=" + Escape(val);
            vals = SplitLine(val, c_line_length);
            int i=0;
            foreach(string v in vals)
            {
                i++;
                string va = v;
                char tc=va[0];
                if(char.IsWhiteSpace(tc) || tc == '#' || tc == '_') va = "_" + va;
                tc = va[va.Length-1];
                if(i < vals.Length) va += '\\';
                else if(char.IsWhiteSpace(tc) || tc == '_') va += "_";
                ret.Add(string.Format("{0}{1}", va, Environment.NewLine));
            }
            return ret.ToArray();
        }
        /// <summary>
        /// Get the output config lines for the config file. Each value can have multiple assigned strings.
        /// Note that comments are not escaped, but values and keys are!
        /// </summary>
        /// <param name="key">The config file key for the entry of interest</param>
        /// <param name="pos">The position of the desired string within pairs/comments.</param>
        /// <param name="pairs">The config key/value file values</param>
        /// <param name="comments">The comments for some keys of interest.</param>
        /// <returns>The lines for the specified entry in a config file. Any given value in comments must have the
        /// same count as the value assigned to a similar key in pairs.</returns>
        static private string[] GetEntry(string key, int pos, Dictionary<string, IList<string>> pairs, Dictionary<string, IList<string>> comments)
        {
            List<string> ret = new List<string>();
            IList<string> val = null;
            string va = pairs[key][pos];
            if(va==null)
            {
                return new string[] { "" };
            }
            string[] vals;
            if(comments != null && comments.TryGetValue(key, out val) && val != null)
            {
                if(val[pos].Length > 0)
                {
                    if(c_line_length > 10)
                    {
                        vals = SplitLine(val[pos], c_line_length - 2);
                        foreach(string v in vals)
                            if(v!=null)
                                ret.Add(string.Format("# {0}{1}", v, Environment.NewLine));
                    }
                    else
                        ret.Add(string.Format("# {0}{1}", val[pos], Environment.NewLine));
                }
            }
            string eq;
            if(pos == 0) eq = "=";
            else eq = "+=";
            va = Escape(key) + eq + Escape(va);
            vals = SplitLine(va, c_line_length);
            int i = 0;
            foreach(string v in vals)
            {
                i++;
                va = v;
                char tc=va[0];
                if(char.IsWhiteSpace(tc)||tc=='#'||tc=='_') va="_"+va;
                tc=va[va.Length-1];
                if(i < vals.Length) va += "\\";
                else if(char.IsWhiteSpace(tc) || tc == '_') va += "_";
                ret.Add(string.Format("{0}{1}", va, Environment.NewLine));
            }
            return ret.ToArray();
        }
        /// <summary>
        /// A utility function for helping with escaped strings - counts the number fo slashes prior to an index.
        /// </summary>
        /// <param name="s">The string of interest</param>
        /// <param name="index">The index to start counting from (actually, index-1).</param>
        /// <returns></returns>
        static public int CountPreSlashes(string s, int index)
        {
            int j = 0;
            for(j = index - 1; j >= 0 && s[j] == '/'; j--) ;
            return index - j - 1;
        }
        /// <summary>
        /// unescapes strings
        ///   \n -> newline
        ///   \r -> carrage return
        ///   \f -> form feed
        ///   \\ -> slash
        ///   \t -> horiz. tab
        ///   \v -> vert. tab
        ///   \[NEWLINE] -> ignored, newline removed
        ///   \0x## -> ASCII ##
        ///   \u#### -> Unicode ####
        /// </summary>
        /// <param name="s">The string to unescape</param>
        /// <returns>The unescaped string</returns>
        static public string UnEscapeString(string s)
        {
            List<object> ret = new List<object>();
            int st = 0;
            for(int i = 0; i < s.Length; i++)
            {
                if(s[i] == '\\')
                {
                    if(i != st)
                    {
                        ret.Add(s.Substring(st, i-st));
                    }
                    i++;
                    if(i == s.Length)
                    {
                        throw new Exception("UnEscaping string, non-escaping '\\' at End of String");
                    }
                    char c = s[i];
                    switch(c)
                    {
                    case 'n':
                        ret.Add(Environment.NewLine);
                        break;
                    case 'r':
                        if(s.Length >= i + 2 && s[i + 1] == '\\' && s[i + 2] == 'n')
                        {
                            i+=2;
                            ret.Add(Environment.NewLine);
                        }
                        else
                            ret.Add(Environment.NewLine);
                        break;
                    case '\\':
                        ret.Add("\\");
                        break;
                    case 'f':
                        ret.Add("\f");
                        break;
                    case 't':
                        ret.Add("\t");
                        break;
                    case 'v':
                        ret.Add("\v");
                        break;
                    case '0':
                        if(i + 2 < s.Length && (s[i] == 'x' || s[i] == 'X'))
                        {
                            ret.Add((byte)Convert.ToByte(s.Substring(i + 1, i + 3)));
                            i += 2;
                            break;
                        }
                        throw new Exception("Error in escaping string, '\\0' must be followed by 'x##', where ## is two numbers.");
                    case 'u':
                        if(i + 4 < s.Length && (s[i] == 'u' || s[i] == 'U'))
                        {
                            ret.Add((byte)Convert.ToByte(s.Substring(i + 1, i + 3)));
                            ret.Add((byte)Convert.ToByte(s.Substring(i + 3, i + 5)));
                            i += 4;
                            break;
                        }
                        throw new Exception("Error in escaping string '\\u' must be followed by '####', where #### is for numbers.");
                    default:
                        ret.Add(s[i]);
                        break;
                    }
                    st = i + 1;
                }
            }
            if(st < s.Length)
            {
                ret.Add(s.Substring(st));
            }
            //merge the string
            if(ret.Count == 1 && ret[0].GetType() == typeof(string))
                return (string)ret[0];
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i < ret.Count; i++)
            {
                object o = ret[i];
                if(o.GetType() == typeof(string))
                {
                    sb.Append((string)o);
                    continue;
                }
                int start = i;
                while(i < ret.Count && ret[i].GetType() == typeof(byte))
                {
                    i++;
                }
                byte[] buff = new byte[i-start];
                for(int j = st; j < i; j++)
                    buff[j-st] = (byte)ret[j];
                sb.Append(Encoding.UTF8.GetString(buff));
            }
            return sb.ToString();
        }
        /// <summary>
        /// This gets the next key/value from the lines loaded from a config file.
        /// </summary>
        /// <param name="file">The name of the file - for printing errors.</param>
        /// <param name="i">The line to start looking, in the file, set to the line past the last line loaded.</param>
        /// <param name="lines">The lines in the file.</param>
        /// <param name="append">This is set to true, if the the key/value is to be appended.</param>
        /// <param name="key">This is set to the key found.</param>
        /// <param name="value">This is set to the value found.</param>
        /// <returns>True if a key/value is found, otherwise false.</returns>
        static public bool GetNextKeyValue(string file, ref int i, string[] lines, out bool append, out string key, out string value)
        {
            int s = i;
            key = null;
            value = null;
            append = false;
            string line = "";
            //get the full line
            while(true)
            {
                if(i == lines.Length)
                {
                    if(line == "") return false;
                    else break;
                }
                string txt = lines[i];
                txt = lines[i];
                txt = txt.Trim();
                i++;
                if(txt.Length == 0) continue; //ignore blank lines.
                if(txt.Substring(0, 1) == "#") continue; //skip comment lines
                if(txt.Substring(0, 1) == "_") txt = txt.Substring(1);
                if(txt.Substring(txt.Length - 1) == "_") txt = txt.Substring(0, txt.Length - 1);
                line += txt;
                if(line.Substring(line.Length - 1) == "\\") line = line.Substring(0, line.Length - 1);
                else break;
            }
            //we have the line, find the assignment
            bool found = false;
            int eq = 0;
            int t1 = 0;
            int t2 = 0;
            while(!found)
            {
                t1 = line.IndexOf('=', eq);
                t2 = line.IndexOf("+=", eq);
                if(t1 < 0 && t2 < 0) throw new Exception("Key/Value without an assignment: " + file + "@" + s.ToString());
                if(t1 > 0 && t2 > 0) eq = Math.Min(t1, t2);
                else eq = Math.Max(t1, t2);
                int ti = eq - 1;
                bool unescaped = true;
                while(ti >= 0 && line[eq] == '\\')
                {
                    unescaped = !unescaped;
                    ti--;
                }
                if(unescaped)
                {
                    found = true;
                    if(line[eq] == '+')
                    {
                        s = eq + 2;
                        append = true;
                    }
                    else s = eq + 1;
                }
                else
                {
                    if(line[eq] == '+') eq += 2;
                    else eq++;
                }
            }
            key = line.Substring(0, eq);
            value = line.Substring(s);
            key = UnEscapeString(key);
            value = UnEscapeString(value);
            return true;
        }
        /// <summary>
        /// This gets the next key/value from the lines loaded from a config file.
        /// </summary>
        /// <param name="file">The name of the file - for printing errors.</param>
        /// <param name="i">The line to start looking, in the file, set to the line past the last line loaded.</param>
        /// <param name="lines">The lines in the file.</param>
        /// <param name="append">This is set to true, if the the key/value is to be appended.</param>
        /// <param name="key">This is set to the key found.</param>
        /// <param name="value">This is set to the value found.</param>
        /// <returns>True if a key/value is found, otherwise false.</returns>
        static public bool GetNextKeyValue(string file, ref int i, string[] lines, out bool append, out string key, out string value, out string comment)
        {
            int s = i;
            key = null;
            value = null;
            comment = null;
            append = false;
            string line = "";
            //get the full line
            bool cfirst = true;
            while(true)
            {
                if(i == lines.Length)
                {
                    if(line == "") return false;
                    else break;
                }
                string txt = lines[i];
                txt = lines[i];
                txt = txt.Trim();
                i++;
                if(txt.Length == 0) continue; //ignore blank lines.
                if(txt.Substring(0, 1) == "#") // read comments
                {
                    if(comment == null)
                    {
                        comment = "";
                    }
                    string c = txt.Substring(1);
                    if(c.Length > 0 && c[0] == ' ') c = c.Substring(1);
                    //check for/handle multi-newlines
                    if(c.Length == 0)
                    {
                        if(comment.Length > 0 && comment[comment.Length - 1] != '\n' && comment[comment.Length - 1] != '\r')
                            comment += Environment.NewLine;
                        comment += Environment.NewLine;
                    }
                    //insert a a space in a comment line break between two non-whitespace, non-dash, non-underscore
                    char t;
                    if(c.Length > 0 && !char.IsWhiteSpace(t=c[0]) && t != '-' && t != '_' &&
                        (comment.Length > 0 && !char.IsWhiteSpace(t=comment[comment.Length - 1]) && t!='-' && t != '_')) comment += " ";
                    comment += c;
                    continue;
                }
                if(txt.Substring(0, 1) == "_") txt = txt.Substring(1);
                if(txt.Substring(txt.Length - 1) == "_") txt = txt.Substring(0, txt.Length - 1);
                line += txt;
                if(line.Substring(line.Length - 1) == "\\") line = line.Substring(0, line.Length - 1);
                else break;
            }
            //we have the line, find the assignment
            bool found = false;
            int eq = 0;
            int t1 = 0;
            int t2 = 0;
            while(!found)
            {
                t1 = line.IndexOf('=', eq);
                t2 = line.IndexOf("+=", eq);
                if(t1 < 0 && t2 < 0) throw new Exception("Key/Value without an assignment: " + file + "@" + s.ToString());
                if(t1 > 0 && t2 > 0) eq = Math.Min(t1, t2);
                else eq = Math.Max(t1, t2);
                int ti = eq - 1;
                bool unescaped = true;
                while(ti >= 0 && line[eq] == '\\')
                {
                    unescaped = !unescaped;
                    ti--;
                }
                if(unescaped)
                {
                    found = true;
                    if(line[eq] == '+')
                    {
                        s = eq + 2;
                        append = true;
                    }
                    else s = eq + 1;
                }
                else
                {
                    if(line[eq] == '+') eq += 2;
                    else eq++;
                }
            }
            key = line.Substring(0, eq);
            value = line.Substring(s);
            key = UnEscapeString(key);
            value = UnEscapeString(value);
            return true;
        }
        /// <summary>
        /// Return an array of strings, splitting the listed string into lines.
        /// </summary>
        /// <param name="s">The line to split</param>
        /// <param name="llen">The lenght of the lines, or &lt;0 for no split.</param>
        /// <returns>The split line</returns>
        static public string[] SplitLine(string s, int llen)
        {
            List<string> ret = new List<string>(s.Split(c_newlines, StringSplitOptions.None));
            for(int i = 0; i < ret.Count; i++)
            {
                if(ret[i].Length >= llen)
                {
                    string line;
                    string rem = ret[i];
                    ret.RemoveAt(i);
                    while(rem.Length > 0)
                    {
                        char check = rem[0];
                        /* this should be handled in the write section - comments SHOULD NOT
                         * have this added
                         */
                        /*if(check == '_' || char.IsWhiteSpace(check))
                        {
                            rem = "_"+rem;
                        }*/

                        int c = Math.Min(llen, rem.Length);
                        int cut;
                        if(c < rem.Length)
                        {
                            cut = c;
                            c = LastWhiteSpace(rem, c);
                            if(c < 0) c = cut;
                        }

                        int escape_count=0, tc=c-1;
                        check = rem[tc];
                        while(tc>=0)
                        {
                            if(rem[tc]=='\\') escape_count++;
                            else break;
                            tc--;
                        }
                        if(check == '_' || char.IsWhiteSpace(check))
                        {
                            escape_count=escape_count%2+1; //we don't want to break up escapes
                            line=rem.Substring(0, c-escape_count);
                            rem=rem.Substring(c-escape_count);
                        }
                        else
                        {
                            escape_count=escape_count%2; //don't split escape pairs
                            line=rem.Substring(0, c-escape_count);
                            rem=rem.Substring(c-escape_count);
                        }

                        ret.Insert(i, line);
                        i++;
                    }
                }
            }
            return ret.ToArray();
        }
        /// <summary>
        /// Escape a string to put in a config file
        /// </summary>
        /// <param name="s">The string to escape</param>
        /// <returns>The escaped string</returns>
        static public string Escape(string s)
        {
            s = s.Replace("\\", "\\\\").Replace("\r\n", "\\n").Replace("\n", "\\n").Replace("\f", "\\f").Replace("\r", "\\n");
            s = s.Replace("\t","\\t").Replace("\v","\\v");
            return s;
        }
        /// <summary>
        /// Find the last whitespace character
        /// </summary>
        /// <param name="s">The string to search</param>
        /// <param name="last">Only accept whitespace at or prior to this position</param>
        /// <returns>Return the position, or -1 if not found</returns>
        static public int LastWhiteSpace(string s, int last)
        {
            do
            {
                if(Char.IsWhiteSpace(s[last])) return last;
                last--;
            }while(last >= 0);
            return -1;
        }

        /// <summary>
        /// Try to intelligently determine the encoder needed to turn readbuff into a string.
        /// </summary>
        /// <param name="readbuff">The buffer to read. It may end up being reversed for big endian UTF-32</param>
        /// <param name="enc">This is changed to the encoder indicated by the BOM, if there is an aparant BOM.</param>
        static public void get_decoder(ref byte[] readbuff, ref Encoding enc)
        {
                    if(readbuff.Length > 2)
                {
                    if(readbuff.Length > 3)
                    {
                        if(readbuff[0] == 0xEF && readbuff[1] == 0xBB && readbuff[2] == 0xBF)
                        {
                            enc = Encoding.UTF8;
                            return;
                        }
                        //this in partiuclar has to be before UTF-16,
                        //as Little Endian UTF-32 starts with the same
                        //first two characters
                        if(readbuff.Length > 4)
                        {
                            if(readbuff.Length % 4 == 0)
                            {
                                if(readbuff[0] == 0xFF && readbuff[1] == 0xFE && readbuff[2] == 0x00 && readbuff[3] == 0x00)
                                {
                                    enc = Encoding.UTF32;
                                    return;
                                }
                                if(readbuff[0] == 0x00 && readbuff[1] == 0x00 && readbuff[2] == 0xFE && readbuff[3] == 0xFF)
                                {
                                    enc = Encoding.UTF32;
                                    //big endian, we need to flip
                                    for(int i = 0; i < readbuff.Length; i += 4)
                                    {
                                        byte c = readbuff[i];
                                        readbuff[i] = readbuff[i + 3];
                                        readbuff[i + 3] = c;

                                        c = readbuff[i + 1];
                                        readbuff[i + 1] = readbuff[i + 2];
                                        readbuff[i + 2] = c;
                                    }
                                    return;
                                }
                            }
                            if(readbuff[0] == 0x2B && readbuff[1] == 0x2F && readbuff[2] == 0x76 &&
                                (readbuff[3] == 0x38 || readbuff[3] == 0x39 || readbuff[3] == 0x2B || readbuff[3] == 0x2F))
                            {
                                enc = Encoding.UTF7;
                                return;
                            }
                        }
                    }
                    if(readbuff.Length % 2 == 0)
                    {
                        if(readbuff[0] == 0xFF && readbuff[1] == 0xFE)
                        {
                            enc = Encoding.Unicode;
                            return;
                        }
                        if(readbuff[0] == 0xFE && readbuff[1] == 0xFF)
                        {
                            enc = Encoding.BigEndianUnicode;
                            return;
                        }
                    }
                }
            }
    }
}
